瀏覽器在繪製出整個頁面前大概經過了哪些步驟呢?什麼原因會阻止瀏覽器繪製頁面?
理解瀏覽器如何運作是網頁效能優化最重要的基礎,從接收到 HTML、CSS、JavaScript 等檔案到繪製出每個像素需要做哪些事情,這一連串的流程就稱為關鍵轉譯路徑(Critical Rendering Path, CRP),本文將會著重在 CRP 的觀念以及如何計算、降低載入頁面的 CRP 來提升網頁效能。
而瀏覽器的詳細轉譯過程會在 Performance - How Rendering Works 解釋。
想要分析頁面載入的 CRP,可以使用以下兩個指標:
使用者從載入網站到開始互動前大概會經過以下步驟:
以這份 index.html
為例:
<html>
<head>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div>Hello World!</div>
<img src="An awesome image.jpg" />
<script src="index.js"></script>
</body>
</html>
開始轉譯網頁前的流程將會是:
可以看到必須等 index.html
、style.css
和 index.js
都解析完畢才能開始顯示頁面,因此有 3 個關鍵資源,由於 style.css
和 index.js
可以同時下載,最短關鍵路徑為 2。
很明顯關鍵資源和關鍵路徑是影響頁面顯示時間的關鍵,數字越大可以開始轉譯頁面的時間就越慢,但是甚麼原因會提升這兩個數字呢?
解析 HTML 的過程中會有許多需要載入的檔案,例如 JavaScript、CSS、圖片等等,其中某些檔案會造成阻塞讓瀏覽器無法開始轉譯,而阻塞又分為兩種,Render blocking 以及 Parser blocking。
瀏覽器需要 HTML 和 CSS 才能繪製出完整的頁面,若在解析完 HTML 當下馬上畫出頁面,等到解析完 CSS 再重新畫出一版頁面,使用者就會看到螢幕閃過幾乎無法閱讀的文字畫面,接著再變為加入 CSS 的頁面,這種現象稱為 Flash of Unstyled Content (FOUC)。
為了避免 FOUC 影響使用者體驗,瀏覽器在解析完 CSS 前會阻止轉譯,CSS 檔案越大、下載時間越久都會延遲瀏覽器能夠開始轉譯頁面的時間。
模擬 FOUC,左為沒有 CSS 的頁面。
為了讓頁面互動性更強,現在的網頁幾乎少不了 JavaScript,也是因為 JavaScript 如此強大能夠操作頁面中的元素、樣式,也成為了繪製頁面的限制。
由於 JavaScript 過於動態,能夠新增或修改 HTML 已解析的元素,甚至是加入 <link>
、<script>
,因此遇到 JavaScript 時瀏覽器會將主線程的控制權從解析 HTML 交給 JavaScript 引擎(阻止解析),執行完畢後再繼續解析 HTML。
但別忘了 JavaScript 還能修改樣式,所以瀏覽器會等到 CSS 都下載、解析完畢才開始執行 JavaScript,這也是為什麼常常看到 <script>
被放在 HTML 的最下方。
Unblocking 的方式有很多,但主要圍繞在以下幾個重點:
用壓縮、Tree shaking、Code spliting 等方式降低關鍵資源的大小,加快下載和解析速度。
把轉譯頁面所需的 CSS、JavaScript 直接寫入 HTML,降低 Round-trip。
例如把手機板的 CSS <link>
加上media
屬性,避免使用電腦時被不必要的 CSS 阻止轉譯。
如果 JavaScript 的執行和 HTML、CSS 無直接關係,在 <script>
加上 async
、defer
屬性後就不會阻止解析。
盡可能的讓關鍵資源越早開始下載越好,因為下載通常是花費最長時間的部分。
另外可以用 Lighthouse 找出 Critical Request Chains
知道了 Blocking 和 Unblocking 的基本觀念後,直接來看看實際的例子吧,為了讓結果更明顯,以下範例皆將低網速為 Slow 3G。另外也可以試著計算關鍵資源和關鍵路徑各為多少。
以下範例皆在 Demo 頁面 Critical Rendering Path 中,可以實際操作觀察結果。
<!DOCTYPE html>
<html>
<head>
<title>No CSS No JavaScript</title>
</head>
<body>
<img src="doge.png" />
<h1 class="main-title">No CSS & No JavaScript</h1>
</body>
</html>
最簡單的狀況,沒有 CSS 和 JavaScript,解析完 HTML 後觸發了 DOMContentLoaded
(藍線)並繪製出頁面,接著在下載完圖片後觸發了 onload
(紅線)。
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="style.css" />
<title>CSS & No JavaScript</title>
</head>
<body>
<img src="doge.png" />
</body>
</html>
加入 CSS 後,遇到 <link>
後開始下載 CSS,但不影響 HTML 的解析,立即觸發 DOMContentLoaded
,下載並解析完 CSS 後繪製出頁面。
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="style.css" />
<title>CSS & JavaScript</title>
</head>
<body>
<img src="doge.png" />
<script src="index.js"></script>
</body>
</html>
加入 CSS 和 JavaScript,因為 Parser blocking,等到解析完 CSS 並執行完 JavaScript 後才觸發 DOMContentLoaded
,開始繪製頁面。
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="style.css" />
<title>CSS & Async JavaScript</title>
</head>
<body>
<img src="doge.png" />
<script src="index.js" async></script>
</body>
</html>
利用 Async 屬性避免 JavaScript 阻止解析,解析完 HTML 後立即觸發了 DOMContentLoaded
。
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="print.css" media="print" />
<title>CSS & Async JavaScript</title>
</head>
<body>
<img src="doge.png" />
<script src="index.js"></script>
</body>
</html>
Non-blocking CSS 被視為較低優先權的 Request 且 JavaScript 不需要等 CSS 解析完畢才執行。(這邊故意增加 CSS 的內容,讓結果更明顯)
透過降低關鍵資源和最低關鍵路徑能有效的加快繪製出頁面的時間,開發網頁時記得檢查是否造成了不必要的等待,並用對應的 Unblocking 方式來加快頁面繪製速度。